#region Microsoft Public License
/*
 * (c) Copyright Chenjun Wu. http://utablesdk.codeplex.com
 * 
 * This source is subject to the Microsoft Public License (Ms-PL).
 * Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
 * All other rights reserved.
 */
#endregion

﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UTable.Input.MultiTouch;
using System.Diagnostics;
using System.Collections.ObjectModel;
using System.Windows;

namespace UTable.Objects.Handlers.Gestures
{
    /// <summary>
    /// Detect gestures within an UI element
    /// All fingers that this gesture engine gets will be captured
    /// </summary>
    public class LocalGestureEngine: MultiTouchEventHandlerClass
    {
        #region Private Fields

        private IInputNode _node = null;

        private UIElement _relativeElement = null;

        private bool _captureFingers = true;

        private bool _handleAllEvents = true;

        private bool _stopRouteDown = true;

        private int _nextGestureInfoId = 0;

        private int _nextContactId = 0;

        private bool _distinguishUser = true;

        #endregion

        #region Properties

        public IInputNode TargetNode
        {
            get { return _node; }
        }

        public UIElement RelativeElement
        {
            get { return _relativeElement; }
        }

        public Dictionary<int, FingerInfo> CurrentFingers = new Dictionary<int, FingerInfo>();

        public Collection<Contact> Contacts = new Collection<Contact>();

        /// <summary>
        /// Indicate whether the fingers handled by this class is captured
        /// The default value is true
        /// </summary>
        public bool CaptureFingers
        {
            get { return _captureFingers; }
            set { _captureFingers = value; }
        }

        /// <summary>
        /// Indicate whether all the inputs handled by this handler should be marked as handled
        /// The default value is true
        /// </summary>
        public Boolean HandleAllEvents
        {
            get { return _handleAllEvents; }
            set { _handleAllEvents = value; }
        }

        /// <summary>
        /// Indicate whether all the inputs handled by this handler should be routed down to its parent node
        /// The default value is true
        /// </summary>
        public Boolean StopRouteDown
        {
            get { return _stopRouteDown; }
            set { _stopRouteDown = value; }
        }

        public bool DistinguishUser
        {
            get { return _distinguishUser; }
            set { _distinguishUser = value; }
        }


        #endregion

        #region Constructor

        public LocalGestureEngine(IInputNode targetNode, UIElement relativeNode)
        {
            _node = targetNode;
            _relativeElement = relativeNode;
        }

        #endregion

        #region Public Methods


        #endregion

        #region Handle Finger Events

        public override void OnMultiTouch(MultiTouchEventArgs args)
        {
            if (TargetNode == null)
                return;

            if (EventRaised != null)
                EventRaised(this, new LocalGestureEngineEvnetArgs(LocalGestureEngineEvnetType.SessionStarted));

            foreach (FingerEventArgs fargs in args.FingerEvents)
            {
                switch (fargs.EventType)
                {
                    case FingerEventType.FINGER_DOWN_EVENT:
                        OnFingerDown(fargs);
                        break;
                    case FingerEventType.FINGER_MOVE_EVENT:
                        OnFingerMove(fargs);
                        break;
                    case FingerEventType.FINGER_UP_EVENT:
                        OnFingerUp(fargs);
                        break;
                    case FingerEventType.FINGER_OUT_EVENT:
                        OnFingerOut(fargs);
                        break;
                    case FingerEventType.FINGER_IN_EVENT:
                        OnFingerIn(fargs);
                        break;
                }
            }

            if (EventRaised != null)
                EventRaised(this, new LocalGestureEngineEvnetArgs(LocalGestureEngineEvnetType.SessionEnded));

            if (HandleAllEvents)
                args.Handled = true;
            if (StopRouteDown)
                args.RouteDown = false;
        }

        private void OnFingerDown(FingerEventArgs args)
        {
            if (CaptureFingers)
                TargetNode.CaptureFinger(args.FingerID);
            CurrentFingers.Add(args.FingerID, new FingerInfo(args));
            
            // a finger-down event can cause a new contact
            // or just update an existing contact
            if (CurrentGestureInfos.Count!= 0)
            {
                foreach (GestureInfo CurrentGestureInfo in CurrentGestureInfos)
                {
                    foreach (Contact contact in CurrentGestureInfo.Contacts)
                    {
                        if (IsPointOnContact(args.Position, contact) && (!DistinguishUser || contact.UserID == args.UserID))
                        {
                            // just update the contact
                            OnContactFingerAdded(contact, args);

                            return;
                        }
                    }
                }
            }
            // create a new contact
            OnContactCreated(args);
 
        }

        private void OnFingerMove(FingerEventArgs args)
        {
            //TODO: improve the efficiency
            if (CurrentFingers.ContainsKey(args.FingerID))
            {
                CurrentFingers[args.FingerID].CurrentState = args;
                
                // a finger-move event can cause a contact update
                // or create a new contact
                // or delete an existing contact
                Contact contact = GetContactByFinger(args.FingerID);

                if (contact != null)
                {
                    if (contact.Fingers.Count == 1)
                    {
                        OnContactFingerMoved(contact, args);
                    }
                    else
                    {
                        // split the contact if needed
                        contact.Fingers.Remove(args.FingerID);
                        if (IsPointOnContact(args.Position, contact))
                        {
                            contact.Fingers.Add(args.FingerID);
                            OnContactFingerMoved(contact, args);
                        }
                        else
                        {
                            UpdateContactPosition(contact);
                            OnContactCreated(args);
                        }
                    }
                }
            }
        }

        private void OnFingerUp(FingerEventArgs args)
        {
            if (CurrentFingers.ContainsKey(args.FingerID))
            {
                // a finger-up event can delete a contact
                // or just update an existing contact
                Contact contact = GetContactByFinger(args.FingerID);
                if (contact != null)
                {
                    contact.CurrentTimestamp = args.Timestamp;
                    contact.TimeTrace = args.TimeTrace;
                    if (contact.Fingers.Count == 1)
                        OnContactRemoved(contact);
                    else
                        OnContactFingerRemoved(contact, args);
                }
                if (CaptureFingers)
                    TargetNode.UncaptureFinger(args.FingerID);
                CurrentFingers.Remove(args.FingerID);
            }
        }

        private void OnFingerIn(FingerEventArgs args)
        {

        }

        private void OnFingerOut(FingerEventArgs args)
        {
            if (CurrentFingers.ContainsKey(args.FingerID))
            {
                // a finger-up event can delete a contact
                // or just update an existing contact
                Contact contact = GetContactByFinger(args.FingerID);
                if (contact != null)
                {
                    contact.CurrentTimestamp = args.Timestamp;
                    contact.TimeTrace = args.TimeTrace;
                    if (contact.Fingers.Count == 1)
                        OnContactRemoved(contact);
                    else
                        OnContactFingerRemoved(contact, args);
                }
                CurrentFingers.Remove(args.FingerID);
            }
        }

        private void OnContactCreated(FingerEventArgs args)
        {
            Contact contact = new Contact(_nextContactId++);
            contact.Fingers.Add(args.FingerID);
            contact.CurrentPosition = args.Position;
            contact.StartPosition = args.Position;
            contact.CurrentTimestamp = args.Timestamp;
            contact.TimeTrace = args.TimeTrace;
            contact.UserID = args.UserID;
            contact.DeviceName = args.DeviceName;
            CurrentFingers[args.FingerID].OwnedContact = contact;
            OnContactCreated(contact);
        }

        private void OnContactFingerAdded(Contact contact, FingerEventArgs args)
        {
            contact.Fingers.Add(args.FingerID);
            CurrentFingers[args.FingerID].OwnedContact = contact;
            contact.CurrentTimestamp = args.Timestamp;
            contact.TimeTrace = args.TimeTrace;
            UpdateContactPosition(contact);
        }

        private void OnContactFingerMoved(Contact contact, FingerEventArgs args)
        {
            contact.CurrentTimestamp = args.Timestamp;
            contact.TimeTrace = args.TimeTrace;
            UpdateContactPosition(contact);
        }

        private void OnContactFingerRemoved(Contact contact, FingerEventArgs args)
        {
            contact.Fingers.Remove(args.FingerID);
            contact.CurrentTimestamp = args.Timestamp;
            contact.TimeTrace = args.TimeTrace;
            UpdateContactPosition(contact);
        }

        #endregion

        #region Handle contact events

        public double ContactMinDistance = 50;
        public double ContactMaxDistance = 200;
        public double ContactAveDistance = 200;
        public double GestureMaxDistance = 800;
        private List<GestureInfo> CurrentGestureInfos = new List<GestureInfo>();  // just a gesture one time

        private bool IsPointOnContact(Point p, Contact c)
        {
            double minDistance = double.MaxValue;
            double maxDistance = double.MinValue;
            double aveDistance = 0;
            foreach (int fid in c.Fingers)
            {
                FingerEventArgs t = CurrentFingers[fid].CurrentState;
                double distance = (p - t.Position).Length;
                if (distance < minDistance)
                    minDistance = distance;
                if (distance > maxDistance)
                    maxDistance = distance;
                aveDistance += distance;
            }
            if (c.Fingers.Count == 0)
                aveDistance = double.MaxValue;
            else
                aveDistance /= c.Fingers.Count;

            if (minDistance < ContactMinDistance && maxDistance < ContactMaxDistance && aveDistance < ContactAveDistance)
                return true;
            return false;
        }

        /// <summary>
        /// Calculate the position of the contact according to the fingers
        /// Set the CurrentPosition property with this position
        /// </summary>
        /// <param name="c">the contact to be updated</param>
        private void UpdateContactPosition(Contact c)
        {
            // just calculate the average position of all the points

            double x = 0;
            double y = 0;
            foreach (int i in c.Fingers)
            {
                x += CurrentFingers[i].CurrentState.Position.X;
                y += CurrentFingers[i].CurrentState.Position.Y;
            }
            x /= c.Fingers.Count;
            y /= c.Fingers.Count;
            c.CurrentPosition = new Point(x, y);

            OnContactUpdated(c);
        }

        private Contact GetContactByFinger(int fingerId)
        {
            return CurrentFingers[fingerId].OwnedContact;
        }

        private void OnContactCreated(Contact contact)
        {
            bool isInCurrentGestures = false;
            if (CurrentGestureInfos.Count!=0)
            {
                //check every gesture in currentgestureinfos
                for (int i = 0; i < CurrentGestureInfos.Count;i++ )
                {
                    // check whether the contact can be added to the current gesture
                    double minDistance = double.MaxValue;
                    foreach (Contact c in CurrentGestureInfos[i].Contacts)
                    {
                        double distance = (c.CurrentPosition - contact.CurrentPosition).Length;
                        if (distance < minDistance)
                            minDistance = distance;
                    }

                    // check whether this contact can be fused to current gesture
                    if (minDistance < GestureMaxDistance && (!DistinguishUser || contact.UserID == CurrentGestureInfos[i].UserId))
                    {
                        // OK add the contact to current gesture info
                        isInCurrentGestures = true;
                        GestureInfo oldGesture = CurrentGestureInfos[i];
                        OnGestureRemoved(CurrentGestureInfos[i], GestureChangeType.ContactAdded);
                        CurrentGestureInfos[i].Contacts.Add(contact);
                        GestureInfo gesture = new GestureInfo(_nextGestureInfoId++, oldGesture.UserId, oldGesture.DeviceName, oldGesture.Contacts);
                        CurrentGestureInfos[i] = gesture;
                        OnGestureStarted(gesture, GestureChangeType.ContactAdded);
                        break;
                    }
                }
            }
            //not in current gestures, create a new one
            if (isInCurrentGestures == false)
            {
                List<Contact> contacts = new List<Contact>();
                contacts.Add(contact);
                GestureInfo gesture = new GestureInfo(_nextGestureInfoId++, contact.UserID, contact.DeviceName, contacts);
                OnGestureStarted(gesture, GestureChangeType.DirectInput);
                CurrentGestureInfos.Add(gesture);
            }

        }

        private void OnContactUpdated(Contact contact)
        {
            foreach (GestureInfo CurrentGestureInfo in CurrentGestureInfos)
            {
                if (CurrentGestureInfo.Contacts.Contains(contact))
                {
                    OnGestureUpdated(CurrentGestureInfo);
                    break;
                }
            }
        }

        private void OnContactRemoved(Contact contact)
        {
            for(int i=0;i<CurrentGestureInfos.Count;i++)
            {
                if (CurrentGestureInfos[i].Contacts.Contains(contact))
                {
                    GestureInfo oldGesture = CurrentGestureInfos[i];
                    OnGestureRemoved(CurrentGestureInfos[i], GestureChangeType.ContactRemoved);

                    CurrentGestureInfos[i].Contacts.Remove(contact);
                    if (CurrentGestureInfos[i].Contacts.Count == 0)
                    {
                        CurrentGestureInfos.RemoveAt(i);
                    }
                    else
                    {
                        CurrentGestureInfos[i] = new GestureInfo(_nextGestureInfoId++, oldGesture.UserId, oldGesture.DeviceName, oldGesture.Contacts);
                        OnGestureStarted(CurrentGestureInfos[i], GestureChangeType.ContactRemoved);
                    }
                    break;
                }
            }
        }

        #endregion

        #region Handle gesture info events

        public void OnGestureStarted(GestureInfo gesture, GestureChangeType startType)
        {
            gesture.StartType = startType;
            foreach (GestureRecognizer recognizer in SupportedGestures)
            {
                if (recognizer.ContactNumberRange.UpperBound >= gesture.Contacts.Count && recognizer.ContactNumberRange.LowerBound <= gesture.Contacts.Count)
                {
                    recognizer.StartRecognize(gesture);
                    //if (!gesture.ContinueRecognize)
                    //    break;
                }
            }
        }

        public void OnGestureUpdated(GestureInfo gesture)
        {
            foreach (GestureRecognizer recognizer in SupportedGestures)
            {
                if (recognizer.ContactNumberRange.UpperBound >= gesture.Contacts.Count && recognizer.ContactNumberRange.LowerBound <= gesture.Contacts.Count)
                {
                    recognizer.Recognizing(gesture);
                    //if (!gesture.ContinueRecognize)
                    //    break;
                }
            }
        }

        public void OnGestureRemoved(GestureInfo gesture, GestureChangeType endType)
        {
            gesture.EndType = endType;
            foreach (GestureRecognizer recognizer in SupportedGestures)
            {
                if (recognizer.ContactNumberRange.UpperBound >= gesture.Contacts.Count && recognizer.ContactNumberRange.LowerBound <= gesture.Contacts.Count)
                {
                    recognizer.EndRecognize(gesture);
                    //if (!gesture.ContinueRecognize)
                    //    break;
                }
            }
        }

        #endregion

        #region Supported Gestures

        private List<GestureRecognizer> _supportedGestures = new List<GestureRecognizer>();

        public List<GestureRecognizer> SupportedGestures { get { return _supportedGestures; } }

        public void RegisterGesture(GestureRecognizer recognizer)
        {
            if (!SupportedGestures.Contains(recognizer))
            {
                for (int i = 0; i < SupportedGestures.Count; i++ )
                {
                    if (SupportedGestures[i].Priority < recognizer.Priority)
                    {
                        SupportedGestures.Insert(i, recognizer);
                        recognizer.GestureEngine = this;
                        recognizer.GestureDetected += OnGestureDetected;
                        return;
                    }
                }

                SupportedGestures.Add(recognizer);
                recognizer.GestureEngine = this;
                recognizer.GestureDetected += OnGestureDetected;
            }
        }

        void OnGestureDetected(GestureRecognizer sender, GestureDetectedEventArgs args)
        {
            if (GestureDetected != null)
                GestureDetected(sender, args);
        }

        public void UnregiserGesture(GestureRecognizer recognizer)
        {
            if (SupportedGestures.Contains(recognizer))
            {
                recognizer.GestureDetected -= OnGestureDetected;
                SupportedGestures.Remove(recognizer);
            }
        }

        public event GestureDetectedEventHandler GestureDetected;

        public event LocalGestureEngineEvnetHandler EventRaised;

        #endregion
    }

    public class LocalGestureEngineEvnetArgs
    {
        public LocalGestureEngineEvnetType Type { get; private set; }

        public LocalGestureEngineEvnetArgs(LocalGestureEngineEvnetType type)
        {
            Type = type;
        }
    }

    public enum LocalGestureEngineEvnetType
    {
        SessionStarted,
        SessionEnded,
    }

    public delegate void LocalGestureEngineEvnetHandler(LocalGestureEngine sender, LocalGestureEngineEvnetArgs args);
}
